iOS设计模式 - KVC内部机制&使用场景

前两天面试在KVC的问题上有所欠缺,对于底层了解不够,因此写下这篇文章。这篇文章主要是使用KVC对Key的搜索原理,自己写代码来实现KVC。

关于Apple是如何实现KVC这个问题:

🙃 🙃 我也不知道……

因为没有办法打印出调用过的函数,所以KVC背后的一切还是很神秘的,只能通过几个有限的API来猜想,

对于setValue:forKey: ,在setter存在的情况,KVC 会直接发送set<Key>: 消息赋值;

在setter不在的情况下,且accessInstanceVariablesDirectly方法是true的情况下,会按(_<key>, _<isKey>, <key>, is<Key>)的顺序去查找实例变量,检查其是否存在之后通过object_setIvar给它赋值。

如果都没有就调用setValue:forUNdefinedKey:抛出错误。

对于setValue:forKeyPath:,递归地检查keyPath中的属性是否存在,到达目标路径时再调用setValue:forKey: 完成赋值。

大致上我准备按上面的执行顺序进行实现,由于集合、字典和模型、KeyPath的情况比较复杂,所以我只实现了最普通的存值和取值,下面进入正题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
enum MyError: ErrorType {
case NotExist
case NotNSObjectType
}
extension NSObject {
func setMyValue(value value: AnyObject?, forKey key: String?) throws {
/**判断Key是否是空值*/
if key!.isEmpty {
throw MyError.NotExist
}
/**判断Value是否是空值*/
if value == nil {
self.setMyNilValue(key!)
return
}
/**判断Value是否是NSObject类型*/
if !(value is NSObject) {
throw MyError.NotNSObjectType
}
/**判断是否存在setKey方法,如果有执行这个方法*/
let funcName = "set" + (key?.capitalizedString)! + ":"
let selector = NSSelectorFromString(funcName)
//let selector = Selector.init(funcName)
//let selector = #selector(Address.setCity(_:))
//print(self.respondsToSelector(selector))
if self.respondsToSelector(selector){
self.performSelector((selector), withObject: value)
//print("执行setKey方法")
return
}
/**判断该类中是否存在 key、_key、iskey成员,如果有给这个成员赋值*/
var count: UInt32 = 0
var flag: Bool = false
let iVars = class_copyIvarList(self.dynamicType, &count)
for i in 0..<count {
let iVar = iVars[Int(i)]
let keyName = NSString(CString: ivar_getName(iVar), encoding: NSUTF8StringEncoding)
if keyName == key! || keyName == "_" + key! || keyName == "is" + key!{
flag = true
object_setIvar(self, iVar, value)
//print("\(self) \(iVar) \(value)")
//print("给\(keyName)赋值")
break
}
}
/**如果以上的成员和方法都不存在,调用未找到key的方法*/
if !flag {
self.setMyValue(value!, forUndefinedKey: key!)
}
}
func setMyNilValue(key: String?) {
print("无法修改\(key)的值,因为传入的是空值")
}
func setMyValue(value: AnyObject, forUndefinedKey key: String) {
print("无法将\(key)的值修改为,因为传入的是空值")
}
func myValueForKey(key: String?) throws -> AnyObject? {
/**判断Key是否是空值*/
if key!.isEmpty {
throw MyError.NotExist
}
/**判断是否存在getKey方法,如果有执行这个方法*/
let funcName = "get" + (key?.capitalizedString)! + ":"
let selector = NSSelectorFromString(funcName)
//let selector = Selector.init(funcName)
//let selector = #selector(Address.setCity(_:))
//print(self.respondsToSelector(selector))
if self.respondsToSelector(selector){
let result = self.performSelector(selector)
//print("执行getKey方法")
return result as? AnyObject
}
/**判断该类中是否存在 key、_key、iskey成员,如果有给这个成员赋值*/
var count: UInt32 = 0
var flag: Bool = false
let iVars = class_copyIvarList(self.dynamicType, &count)
for i in 0..<count {
let iVar = iVars[Int(i)]
let keyName = NSString(CString: ivar_getName(iVar), encoding: NSUTF8StringEncoding)
if keyName == key! || keyName == "_" + key! || keyName == "is" + key!{
flag = true
print("取出\(keyName)的值")
//print("\(self) \(iVar) \(value)")
return object_getIvar(self, iVar)
}
}
/**如果以上的成员和方法都不存在,调用未找到key的方法*/
if !flag {
self.myValueForUndefinedKey(key!)
}
return nil
}
func myValueForUndefinedKey(key: String) -> AnyObject?{
print("无法取出\(key)的值,因为属性不存在")
return nil
}
}
/**执行*/
let add = Address()
add._country = "China"
add._province = "HeBei"
add._city = "ShiJiaZhuang"
add._province = "ChangAnQu"
do {
try add.setMyValue(value: "BeiJing", forKey: "city")
try add.setMyValue(value: "USA", forKey: "country")
try add.setMyValue(value: "South", forKey: "")
try add.setMyValue(value: "300169", forKey: "postCode")
print("country:\(add._country) city:\(add._city) province:\(add._province) district\(add._district)")
}
catch MyError.NotExist {
print("key是空的")
}
catch MyError.NotNSObjectType {
print("value不是NSObject类型")
}
catch {
print("传值出错")
}
do {
let country = try add.myValueForKey("country")
let city = try add.myValueForKey("city")
print("----------------/n \(country) \(city)")
}
catch MyError.NotExist {
print("key是空的")
}
catch {
print("取值出错")
}

以上是根据KVC执行顺序自定义的一段代码,当然也省略了一些功能,总体上的逻辑还是比较清楚的,但是有一点问题,在使用object_setIvarobject_getIvar这两个runtime函数时,程序会频繁崩溃,而自己对runtime认识不足,以后再完整这段代码吧。

关于KVC,除去存取方法,底层执行顺序,错误处理以外,还有几点需要了解:

Value合法性校验

KVC提供了校验key对应的value是否合法的方法:

1
2
public func validateValue(ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey inKey: String) throws
public func validateValue(ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKeyPath inKeyPath: String) throws

如果使用这个方法,需要进行重写,它的使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Address: NSObject {
var _country: String?
override func validateValue(ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey inKey: String) throws {
let country = ioValue.memory as! String
if country == "Janpan" {
throw MyError.NotExist
}
throw MyError.Nothing
}
}
/**执行*/
let add = Address()
var value: AnyObject? = "Janpan"
do {
try add.validateValue(&value, forKey: "_country")
}
catch MyError.NotExist {
print("输入有误,请重新输入")
}
catch MyError.Nothing {
add.setValue(value, forKey: "_country")
}
catch {}
print(add._country)
/**当 var value: AnyObject? = "Janpan" 时输出*/
输入有误,请重新输入
nil
/**当 var value: AnyObject? = "Chain" 时输出*/
Optional("China")

这样就实现了先对传入值的合法性进行校验,无误后再进行赋值,如上面的代码,当我们需要验证能不能用KVC设定某个值时,可以将其重写后调用

validateValue: forKey:

这个方法来验证,那么KVC就会直接调用这个方法来返回。需要注意的是即使重写了该方法,如果我们没有主动进行调用,KVC也不会主动去做验证,所以这个方法需要手动调用。

KVC的应用场景

作为iOS平台的知名黑魔法,KVC有很多奇特的作用,KVC在iOS开发中是绝不可少的利器,这种基于运行时的编程方式极大地提高了灵活性,简化了代码,它的常见作用有以下几点:

动态的存值和取值

🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃 🙃

访问和修改私有的成员

对于类里的私有属性,无论Swift还是Objective-C都是无法直接访问的,但是KVC是可以的。

Model和字典的互相转换

这是KVC又一个强大之处,只需要很少的代码量即可完成很多功能。

修改控件的内部属性

这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple没有提供这访问这些控件的API,这样我们就无法正常地访问和修改这些控件的样式。

而KVC在大多数情况可下可以解决这个问题。比如个性化UITextField中的placeHolderText,一般情况下可以运用runtime来获取Apple不想开放的属性名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**执行*/
let count:UnsafeMutablePointer<UInt32> = nil
var properties = class_copyIvarList(UITextField.self, count)
while properties.memory.debugDescription != "0x0000000000000000"{
let t = ivar_getName(properties.memory)
let n = NSString(CString: t, encoding: NSUTF8StringEncoding)
print(n)
properties = properties.successor()
}
/**输出*/
......
Optional(_background)
Optional(_disabledBackground)
Optional(_clearButtonMode)
Optional(_leftView)
Optional(_leftViewMode)
Optional(_rightView)
Optional(_rightViewMode)
Optional(_traits)
Optional(_nonAtomTraits)
Optional(_fullFontSize)
Optional(_padding)
Optional(_selectionRangeWhenNotEditing)
......

可以从里面看到其他还有很多东西可以修改,运用KVC设值可以获得自己想要的效果。

KVC操作集合

Apple对KVC的valueForKey:方法作了一些特殊的实现,比如说NSArray和NSSet这样的容器类就实现了这些方法,所以可以用KVC很方便地操作集合。

高阶信息传递

当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**执行*/
let arrStr: NSArray = ["english", "franch", "chinese"]
let arrCapStr = arrStr.value(forKey: "capitalizedString")
print(arrCapStr as! NSArray)
let arrStrLength = arrStr.value(forKeyPath: "capitalizedString.length")
print(arrStrLength! as! NSArray)
/**输出*/
(
English,
Franch,
Chinese
)
(
7,
6,
7
)

方法capitalizedString被传递到NSArray中的每一项,这样,NSArray的每一员都会执行capitalizedString并返回一个包含结果的新的NSArray。从打印结果可以看出,所有String都成功以转成了大写。
同样如果要执行多个方法也可以用valueForKeyPath:方法。它先会对每一个成员调用 capitalizedString方法,然后再调用length,因为lenth方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里。

KVO

当然了,KVC除了传值取值,访问和修改私有变量,修改控件属性,操作集合、字典和模型,更重要的就是KVO了。

Demo下载请点击这里